#pragma once

#include <vector>

#ifdef PUNCH_MSGPACK
#include <msgpack.hpp>
#endif

#include "../BaseBook.h"
#include "../BaseTracker.h"
#include "order.h"
#include "mktpool.h"

namespace mdtx::punch::tradesim
{
    template <class QT>
    class Engine
    {
    public:
        using order_type = Order<QT>;
        using time_type = typename QT::time_type;
        using id_type = typename QT::id_type;
        using quant_type = typename QT::quant_type;

        using book_type = BaseBook<QT>;
        using tracker_type = BaseTracker<QT>;
        using mktpool_type = MktPool<QT>;

        using book_ptr = std::shared_ptr<book_type>;
        using tracker_ptr = std::shared_ptr<tracker_type>;
        using mktpool_ptr = std::shared_ptr<mktpool_type>;

    private:
        mktpool_ptr mkt;
        book_ptr book;
        tracker_ptr tracker;

        using pool_rec = typename mktpool_type::MktPoolT;

        /**
         * @brief Execute order against queue BBO
         *
         * @tparam Queue order queue type
         * @param order aggressor order
         * @param queue order queue
         */
        template <class Queue>
        void exec_queue(order_type &order, Queue &queue)
        {
            quant_type price, quant, tnvr;
            auto &bbo = queue.front();
            auto &resting = book->order(bbo.ins_id, bbo.seq_id);
            price = bbo.price;
            if (resting.quant > order.quant)
            {
                quant = order.quant;
                // order
                order.quant = 0;
                order.set_fill();
                // resting
                resting.quant -= quant;
                resting.set_match();
                // queue
                bbo.quant = resting.quant;
            }
            else
            {
                quant = resting.quant;
                // order
                order.quant -= quant;
                order.set_match();
                // resting
                resting.quant = 0;
                resting.set_fill();
                // queue
                queue.pop_front();
            }
            // tracker callback
            tnvr = price * quant;
            tracker->on_match(order, price, quant, tnvr, true);
            tracker->on_match(resting, price, quant, tnvr, false);
            // orderbook callback
            book->on_order(order.ins_id, order);
            book->on_order(resting.ins_id, resting);
        }

        /**
         * @brief Execute market pool against queue bbo
         *
         * @tparam Queue order queue type
         * @param pool market pool (acts as aggressor)
         * @param queue order queue
         * @param price matched price
         */
        template <typename Queue>
        void exec_queue(pool_rec &pool, Queue &queue, quant_type price)
        {
            quant_type quant, tnvr;
            auto &bbo = queue.front();
            auto &resting = book->order(bbo.ins_id, bbo.seq_id);
            if (resting.quant > pool.vol)
            {
                quant = pool.vol;
                tnvr = price * quant;
                // pool
                pool.vol = 0;
                pool.tnvr -= tnvr;
                pool.norder -= 1;
                // resting
                resting.quant -= quant;
                resting.set_match();
                // queue
                bbo.quant = resting.quant;
            }
            else
            {
                quant = resting.quant;
                tnvr = price * quant;
                // pool
                pool.vol -= quant;
                pool.tnvr -= tnvr;
                pool.norder -= 1;
                // resting
                resting.quant = 0;
                resting.set_fill();
                // queue
                queue.pop_front();
            }
            // tracker callback
            tracker->on_match(resting, price, quant, tnvr, false);
            // orderbook callback
            book->on_order(resting.ins_id, resting);
        }

        /**
         * @brief Execute order against market pool
         *
         * @param order aggressor order
         * @param pool market pool
         * @param price matched price
         */
        void exec_pool(order_type &order, pool_rec &pool, quant_type price)
        {
            quant_type quant, tnvr;
            if (pool.vol > order.quant)
            {
                quant = order.quant;
                tnvr = price * quant;
                // pool
                pool.vol -= quant;
                pool.tnvr -= tnvr;
                pool.norder -= 1;
                // order
                order.quant = 0;
                order.set_fill();
            }
            else
            {
                quant = pool.vol;
                tnvr = price * quant;
                // pool
                pool.vol = 0;
                pool.tnvr -= tnvr;
                pool.norder -= 1;
                // order
                order.quant -= quant;
                order.set_match();
            }
            tnvr = price * quant;
            // tracker callback
            tracker->on_match(order, price, quant, tnvr, true);
            // orderbook callback
            book->on_order(order.ins_id, order);
        }

        /**
         * @brief Match market buy order
         *
         * @param order aggressor order
         */
        void match_mkt_buy(order_type &order)
        {
            auto &pool = mkt->get(order.ins_id);
            auto &offer = book->ask(order.ins_id);
            if (pool.avail())
            {
                // we have liquidity in market pool, ask BBO should be higher than pool.high
                assert(offer.empty() || (offer.front().price > pool.high));
                // VWAP exec'ed
                exec_pool(order, pool, pool.price);
            }
            else
            {
                while (order.quant > 0 && !offer.empty())
                {
                    exec_queue(order, offer);
                }
            }
        }

        /**
         * @brief Match market sell order
         *
         * @param order aggressor order
         */
        void match_mkt_sell(order_type &order)
        {
            auto &pool = mkt->get(order.ins_id);
            auto &offer = book->bid(order.ins_id);
            if (pool.avail())
            {
                // we have liquidity in market pool, bid BBO should be lower than pool.low
                assert(offer.empty() || (offer.front().price < pool.low));
                // VWAP exec'ed
                exec_pool(order, pool, pool.price);
            }
            else
            {
                while (order.quant > 0 && !offer.empty())
                {
                    exec_queue(order, offer);
                }
            }
        }

        /**
         * @brief Match limit buy order
         *
         * @param order aggressor order
         */
        void match_lmt_buy(order_type &order)
        {
            auto &pool = mkt->get(order.ins_id);
            auto &offer = book->ask(order.ins_id);
            if (pool.avail())
            {
                // we have liquidity in market pool, ask BBO should be higher than pool.high
                assert(offer.empty() || (offer.front().price > pool.high));
                if (order.price > pool.low)
                {
                    // can't buy higher than pool.high
                    exec_pool(order, pool, std::min(order.price, pool.high));
                }
            }
            else
            {
                while (order.quant > 0 && !offer.empty())
                {
                    if (offer.front().price > order.price)
                    {
                        break;
                    }
                    else
                    {
                        exec_queue(order, offer);
                    }
                }
            }
        }

        /**
         * @brief Match limit sell order
         *
         * @param order aggressor order
         */
        void match_lmt_sell(order_type &order)
        {
            auto &pool = mkt->get(order.ins_id);
            auto &offer = book->bid(order.ins_id);
            if (pool.avail())
            {
                // we have liquidity in market pool, bid BBO should be lower than pool.low
                assert(offer.empty() || (offer.front().price < pool.low));
                if (order.price < pool.high)
                {
                    // can't sell lower than pool.low
                    exec_pool(order, pool, std::max(order.price, pool.low));
                }
            }
            else
            {
                while (order.quant > 0 && !offer.empty())
                {
                    if (offer.front().price < order.price)
                    {
                        break;
                    }
                    else
                    {
                        exec_queue(order, offer);
                    }
                }
            }
        }

        /**
         * @brief Update market pool and queues after set tick/bar
         *
         * @param pool market pool
         * @param ins_id instrument id
         */
        void update_pool(id_type ins_id, pool_rec &pool)
        {
            auto &bid = book->bid(ins_id);
            auto &ask = book->ask(ins_id);
            bool bid_match, ask_match, avail;
            while (1)
            {
                avail = pool.avail();
                bid_match = !bid.empty() && avail && bid.front().price >= pool.low;
                ask_match = !ask.empty() && avail && ask.front().price <= pool.high;
                if (bid_match && ask_match)
                {
                    if (bid.front().seq_id < ask.front().seq_id)
                    {
                        exec_queue(pool, bid, pool.low);
                    }
                    else
                    {
                        exec_queue(pool, ask, pool.high);
                    }
                }
                else if (bid_match)
                {
                    exec_queue(pool, bid, pool.low);
                }
                else if (ask_match)
                {
                    exec_queue(pool, ask, pool.high);
                }
                else
                {
                    break;
                }
            }
        }

        /**
         * @brief Cancel an order
         *
         * @param order order
         */
        void cancel_order(order_type &order)
        {
            auto cancel = order.quant;
            order.quant = 0;
            order.set_cancel();
            // tracker callback
            tracker->on_cancel(order, cancel);
            // orderbook callback
            book->on_order(order.ins_id, order);
        }

        /**
         * @brief Cancel all orders belong to a client in queue
         *
         * @tparam Queue queue type
         * @param ins_id instrument id
         * @param cli_id client id
         * @param queue ask/bid queue
         */
        template <class Queue>
        void cancel_queue(id_type ins_id, id_type cli_id, Queue &queue)
        {
            auto it = std::remove_if(queue.begin(), queue.end(),
                                     [&ins_id, &cli_id, this](const auto &quote)
                                     {
                                         bool pred = quote.cli_id == cli_id;
                                         if (pred)
                                         {
                                             auto &order = book->order(ins_id, quote.seq_id);
                                             cancel_order(order);
                                         }
                                         return pred;
                                     });
            queue.erase(it, queue.end());
        }

        /**
         * @brief Cancel a client's BBO order in queue
         *
         * @tparam Queue queue type
         * @param ins_id instrument id
         * @param cli_id client id
         * @param queue ask/bid queue
         */
        template <class Queue>
        void cancel_queue_bbo(id_type ins_id, id_type cli_id, Queue &queue)
        {
            for (auto it = queue.begin(); it != queue.end(); ++it)
            {
                if (it->cli_id == cli_id)
                {
                    auto &order = book->order(ins_id, it->seq_id);
                    cancel_order(order);
                    queue.erase(it);
                    break;
                }
            }
        }

    public:
        /**
         * @brief Submit an order
         *
         * @param order order
         */
        void submit(const order_type &order)
        {
            // insert order to book
            auto ins_id = order.ins_id;
            auto cli_id = order.cli_id;
            auto &new_order = book->new_order(ins_id, order);

            auto &pool = mkt->get(ins_id);
            if (new_order.is_market())
            {
                if (new_order.is_buy())
                {
                    match_mkt_buy(new_order);
                }
                else
                {
                    match_mkt_sell(new_order);
                }
                if (new_order.quant > 0)
                {
                    cancel_order(new_order);
                }
                else
                {
                    new_order.set_fill();
                }
            }
            else
            {
                if (new_order.is_buy())
                {
                    match_lmt_buy(new_order);
                    if (new_order.quant > 0)
                    {
                        book->bid(ins_id).insert(new_order);
                        new_order.set_queue();
                        tracker->on_queue(new_order);
                    }
                    else
                    {
                        new_order.set_fill();
                    }
                }
                else
                {
                    match_lmt_sell(new_order);
                    if (new_order.quant > 0)
                    {
                        book->ask(ins_id).insert(new_order);
                        new_order.set_queue();
                        tracker->on_queue(new_order);
                    }
                    else
                    {
                        new_order.set_fill();
                    }
                }
            }
        }

#ifdef PUNCH_MSGPACK
        /**
         * @brief Submit an order or a cancel order command via a serialised buffer
         *
         * @details normal order is deserialised and submitted. An order set_cancel()
         * is considered a cancel order command and if set_aon() cancel_buy / cancel_sell
         * is called depending on order side. Instrument and client id must be correctly
         * set.
         *
         * @param buf a buffer
         * @param len buffer size
         */
        void submit(const char *buf, size_t len)
        {
            order_type order = msgpack::unpack(buf, len).get().as<order_type>();
            if (order.is_cancel())
            {
                // cancel order command received
                if (order.is_aon())
                {
                    // cancel all
                    if (order.is_buy())
                    {
                        cancel_buy(order.ins_id, order.cli_id);
                    }
                    else
                    {
                        cancel_sell(order.ins_id, order.cli_id);
                    }
                }
                else
                {
                    // cancel order
                    auto cli_id = book->order(order.ins_id, order.seq_id).cli_id;
                    if (order.cli_id == cli_id)
                    {
                        // only cancel if cli_id is correctly set
                        cancel(order.ins_id, order.seq_id, order.quant);
                    }
                }
            }
            else
            {
                // normal order to submit
                submit(order);
            }
        }
#endif

        void set_tick(id_type ins_id, time_type time, quant_type price, quant_type vol, quant_type tnvr,
                      quant_type norder, quant_type bid_bbo, quant_type ask_bbo)
        {
            auto &pool = mkt->set_tick(ins_id, time, price, vol, tnvr,
                                       norder, bid_bbo, ask_bbo);
            update_pool(ins_id, pool);
        }

        void set_bar(id_type ins_id, time_type time, quant_type high, quant_type low, quant_type vol, quant_type tnvr,
                     quant_type vwap, quant_type norder)
        {
            auto &pool = mkt->set_bar(ins_id, time, high, low, vol, tnvr,
                                      vwap, norder);
            update_pool(ins_id, pool);
        }

#ifdef PUNCH_MSGPACK
        /**
         * @brief Set pool from a serialised buffer
         *
         * @param buf buffer
         * @param len buffer size
         */
        void set_buf(const char *buf, size_t len)
        {
            // TODO: msgpack throws, should consider a robust error handling logic
            size_t offset = 0;
            pool_rec new_pool;
            std::vector<id_type> ins_id = msgpack::unpack(buf, len, offset).get().as<decltype(ins_id)>();
            for (auto ins : ins_id)
            {
                msgpack::unpack(buf, len, offset).get().convert(new_pool);
                // Don't time travel
                if (mkt->get(ins).time < new_pool.time)
                {
                    auto &pool = mkt->set_raw(ins, new_pool);
                    update_pool(ins, pool);
                }
            }
        }
#endif

        void cancel(id_type ins_id, id_type seq_id, quant_type quant)
        {
            if (seq_id >= book->num_order(ins_id))
            {
                return;
            }

            auto &order = book->order(ins_id, seq_id);
            if (order.is_fill() || order.is_cancel())
            {
                return;
            }

            if (quant == 0 || quant >= order.quant)
            {
                quant = order.quant;
                order.quant = 0;
                order.set_cancel();
                if (order.is_buy())
                {
                    book->bid(ins_id).erase(order);
                }
                else
                {
                    book->ask(ins_id).erase(order);
                }
            }
            else
            {
                order.quant -= quant;
                if (order.is_buy())
                {
                    book->bid(ins_id).find(order)->quant = order.quant;
                }
                else
                {
                    book->ask(ins_id).find(order)->quant = order.quant;
                }
            }
            // tracker callback
            tracker->on_cancel(order, quant);
            // orderbook callback
            book->on_order(ins_id, order);
        }

        void cancel_buy(id_type ins_id, id_type cli_id)
        {
            cancel_queue(ins_id, cli_id, book->bid(ins_id));
        }

        void cancel_sell(id_type ins_id, id_type cli_id)
        {
            cancel_queue(ins_id, cli_id, book->ask(ins_id));
        }

        void cancel_bbo_buy(id_type ins_id, id_type cli_id)
        {
            cancel_queue_bbo(ins_id, cli_id, book->bid(ins_id));
        }

        void cancel_bbo_sell(id_type ins_id, id_type cli_id)
        {
            cancel_queue_bbo(ins_id, cli_id, book->bid(ins_id));
        }
    };
}
